查看原文
其他

SPI原理以及SPI在Android中的实战

搜狐媒体 刘欢 搜狐技术产品 2022-12-21


  

本文字数:13862

预计阅读时间:35分钟

1、前言

在开题之前大家有没有这个疑惑,就是随着业务的不断发展和壮大,我们的工程结构以及代码量越来越大,交织在一起,你中有我,我 中有你,导致项目结构分工不是很明确,职责不清晰,如果线上有紧急问题需要排查的时候,我们可能就手忙脚忙了,有时候也不知道问题出现在哪个模块,导致问题排查效率低下,同时不利于项目复盘和明确职责,又或者老板想看下业务成功率,服务成功率等等一些数据的时候,我们可能为了方便直接在代码里面写了,随着业务的不断迭代,新接手的同事可能就会绞尽脑汁,这段代码到底是干什么的,为什么要写在这里(也许这段代码根本没有任何的实际业务含义,就是一个埋点或者数据统计方面的代码)等等这些问题,今天我们隆重的给大家介绍一种新的解决方案来解决这些问题,这个方案就是SPI,全称是Service Provider Interface,当然大家可能说也有别的方案,是,确实是有别的方案,条条道路通罗马,OK,我们进入正题。

2、什么是SPI

SPI(Service Provider Interface),是JDK提供的一套用来被第三方实现或者扩展的API,它是一种JVM层面的服务注册发现机制, 可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。SPI机制主要思想是将装配的控制权移到程序之外,在组件化设计中这个机制尤其重要,其核心思想就是解耦。

  • SPI整体机制。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,最核心的思想就是服务注册+服务发现
  • SPI和API区别。API

   SPI 

为了更清楚的把这个问题讲明白,我们使用具体的图来说明SPI与API区别,上图就很清晰的说明了这两个问题

一般模块之间通信基本上都是通过接口,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口概念”。当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是API,这种接口和实现都是放在实现方的。接口和实现方属于同一个模块,密切不可分割。当接口存在于调用方这边时,就是SPI,由接口调用方确定接口规则,然后由不同的具体业务去根据这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:一个电脑制造公司,设计好了充电器标准图纸以后,那么接下来就可以把这个图纸分发给不同的厂商去生产,最后只要严格按照图纸要求,就可以生产合格的商品。通过上面的图2和图3以及配合上面的文字介绍,相信大家应该很非常清楚API和SPI的区别了。

3、SPI作用

SPI的发现能力是不需要依赖于其他类库,最重要的作用就是解耦 主要实现方式是。

  • java.util.ServiceLoader#load JDK自身提供的加载能力

4、实现原理

源码分析:
  • ServiceLoader源码
public final class ServiceLoader<S>
implements Iterable<S>
{
//配置文件所在的包目录路径
private static final String PREFIX = "META-INF/services/";
 
// 接口名称
private final Class<S> service;
 
// 类加载器
private final ClassLoader loader;
 
// The access control context taken when the ServiceLoader is created
// Android-changed: do not use legacy security code.
// private final AccessControlContext acc;
 
//providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
 
// //内部类LazyIterator的实例
private LazyIterator lookupIterator;
 
  
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
 
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// Android-changed: Do not use legacy security code.
// On Android, System.getSecurityManager() is always null.
// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
 
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
 
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
 
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
 
  
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
 
  
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in"utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
 
  
private class LazyIterator
implements Iterator<S>
{
 
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
 
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
 
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
 
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
// Android-changed: Let the ServiceConfigurationError have a cause.
"Provider " + cn + " not found", x);
// "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
// Android-changed: Let the ServiceConfigurationError have a cause.
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service,
"Provider " + cn + " not a subtype", cce);
// fail(service,
// "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
 
public boolean hasNext() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return hasNextService();
/*
else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
 
public S next() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return nextService();
/*
else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
 
public void remove() {
throw new UnsupportedOperationException();
}
 
}
 
  
public Iterator<S> iterator() {
return new Iterator<S>() {
 
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
 
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
 
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
 
public void remove() {
throw new UnsupportedOperationException();
}
 
};
}
 
  
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
 
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
 
/**
* Creates a new service loader for the given service type, using the
* extension class loader.
*
* <p> This convenience method simply locates the extension class loader,
* call it <tt><i>extClassLoader</i></tt>, and then returns
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>, <i>extClassLoader</i>)</pre></blockquote>
*
* <p> If the extension class loader cannot be found then the system class
* loader is used; if there is no system class loader then the bootstrap
* class loader is used.
*
* <p> This method is intended for use when only installed providers are
* desired. The resulting service will only find and load providers that
* have been installed into the current Java virtual machine; providers on
* the application'
s class path will be ignored.
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
 
  
public static <S> S loadFromSystemProperty(final Class<S> service) {
try {
final String className = System.getProperty(service.getName());
if (className != null) {
Class<?> c = ClassLoader.getSystemClassLoader().loadClass(className);
return (S) c.newInstance();
}
return null;
} catch (Exception e) {
throw new Error(e);
}
}
// END Android-added: loadFromSystemProperty(), for internal use.
 
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
 
}

4.1 ServiceLoader.load加载入口,整个方法的入口是java.util.ServiceLoader#load 为入口,将当前接口Class类型及其类加载器传入至Loader变量中

@CallerSensitive
 public static <S> ServiceLoader<S> load(Class<S> service) {
     ClassLoader cl = Thread.currentThread().getContextClassLoader();
     return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
 }

loader.iterator() 返回一个迭代器。首先会到providers中去查找有没有存在的实例,有就直接返回,没有再到LazyIterator中查找变量传入之后,初始化类:LazyIterator,从名称就可以看出来这是一个懒加载的迭代器,只有真正使用触发时才会进行实例的,初始化,核心初始化逻辑在方法:java.util.ServiceLoader.

LazyIterator#hasNextService中

//其他代码忽略
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
 
 
//其他代码忽略
总体的实现步骤:
  • 首先拿到配置文件名fullName
  • 通过类加载器获得所有模块的配置文件
  • 依次扫描每个配置文件的内容,返回配置文件内容Iterator pending,每个配置文件中可能有多个实现类的全限定名,所以pending也是个迭代器

4.2 分析nextService方法

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found", x);
    }
    if (!service.isAssignableFrom(c)) {
        ClassCastException cce = new ClassCastException(
                service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
        fail(service,
             "Provider " + cn  + " not a subtype", cce);
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}
  • 首先根据nextName,Class.forName加载拿到具体实现类的class对象
  • Class.newInstance()实例化拿到具体实现类的实例对象
  • 将实例对象转换service.cast为接口
  • 返回实例对象

5、应用场景

  • WEB中的应用 JDBC、Spring、Dubbo、Common-Logging、Hotspot
  • Android中的应用 在Android的组件化方案中,有一种便是通过AutoService + ServiceLoader+APT+Gradle插件的方式,自动生成META-INF/services/xxx配置文件,以实现业务Module之间的交互(跳转、传参...),具体的做法如下:

开发阶段:对关联的类使用编译期注解 定义注解处理器

apply plugin: 'java'
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //要处理的注解
    implementation project(path: ':annotation')
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc3'
    //这块在gradle3.4之后有这个坑,必须加上annotationProcessor,不然生不成注解文件
    compile 'com.squareup:javapoet:1.8.0'
}
 
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

编译阶段:通过注解处理器遍历查找被指定注解修饰的类的信息,收集写入到 META-INF/services 目录下的文件中。

private void generateSpiConfigFiles(){
        log("generateConfigFiles start");
        for(String interfaceName : servicesMap.keySet()){
            String resourceFile = "META-INF/services/serviceloader/" + interfaceName;
            try {
                FileObject exitFile = filer.getResource(StandardLocation.CLASS_OUTPUT_PATH,"p",resourceFile);
                exitFile.delete();
            } catch (IOException e) {
                LogUtil.d("resource file did not already exit.");
            }
            Map<String,String> interfaceMap = servicesMap.get(interfaceName);
            if(interfaceMap == null) continue;
            FileObject fileObject = null;
            try {
                fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT_PATH,"", resourceFile);
                log("fileObject="+fileObject.toUri().toString());
                Writer writer = fileObject.openWriter();
                for (String key : interfaceMap.keySet()){
                    writer.write(key + " : " + interfaceMap.get(key));
                    writer.write("\n");
                }
                writer.flush();
                writer.close();
 
            } catch (IOException e) {
                LogUtil.d("generateConfigFiles exception" + e.getStackTrace());
                return;
            }
 
        }
        LogUtil.d("generateSpiConfigFiles end!!!!");
    }

运行阶段:通过 ServiceLoader 查找 META-INF/services 目录下指定的文件,解析文件的内容,获取要加载的类信息

6、 代码实现

  • 项目示例截图

文字显示的这三块代表三种不同的业务场景,这里只是举个例子,抛砖引玉,大家可以根据自己的实际情况,采用serviceload开发自己的业务

  • SPI在Android应用架构图实际项目开发过程中,业务模块这里都可以打包成具体的AAR,放在maven仓库,各个业务线开发是互相独立的
  • 定义home_page接口
package com.example.api;
 
public interface IHome {
    String show();
}
  • home_page_api module build.gradle文件配置
  • 定义home_page服务

创建home_page module

package com.example.module;
 
import com.example.api.IHome;
 
 
public class HomeService implements IHome {
    private static final String TAG = HomeService.class.getSimpleName();
 
    @Override
    public String show() {
        return "I am HomeService ";
    }
}

home_page module build.gradle配置

  • 在壳子工程app 使用

build.gradle中依赖各个服务和接口module

dependencies {
 
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
 
 
 
    implementation project(path: ':home_page_module')
    implementation project(path: ':home_page_module_api')
 
    implementation project(path: ':login_module')
    implementation project(path: ':login_module_api')
 
    implementation project(path: ':order_module_api')
    implementation project(path: ':order_module')
}
  • APP壳子集成使用

按照上述的步骤就可以实现业务解耦,达到高内聚,低耦合的标准,同时大大提高项目的可维护性,APP壳子是非常轻量级的一层,只做最基本的集成

职责,或者是一些flavors的配置,启动数据等等的基本配置信息,配合上 flavors那么就可以一次打包生成不同的产物,根据不同的flavors替换不同的

服务,各个业务线去完成具体的业务逻辑。

最后附上源码地址:

https://github.com/zhaoqiang1991/providerdemo

7、FAQ

  • SPI优点: spi技术适合在一些大型的项目中,能够大大地提高接口设计的灵活性,解耦,高内聚,提升了团队协作的效率,明确了业务边界
  • SPI缺点

(1):spi 不适合小型项目,因为会多出来很多的module,同时项目架构比较复杂,无疑增加了代码阅读的难度

(2):遍历加载所有的实现类,效率还是相对较低的(初次启动的时候,加载过以后缓存起来);

(3):当多个 ServiceLoader 同时 load 时,会有并发问题。

综上所述:一个技术是不是适合所在的项目,要具体情况具体分析,不是说那个技术好就适用于本项目。

参考:

https://blog.csdn.net/ecjtuhq/article/details/107552479

https://zhuanlan.zhihu.com/p/436560515

结束,感谢阅读!


也许你还想看

(▼点击文章标题或封面查看)

响应式异步非阻塞编程在服务端的应用

2022-09-15

你真的懂iOS的异常捕获吗?

2022-09-08

响应式编程(Reactive Programming)初探

2022-08-25

Android网络请求优化及离线上传封装

2022-08-18

Jetpack Compose自定义Layout详解

2022-08-11


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存